#ifndef __VECTOR_3_HPP__
#define __VECTOR_3_HPP__

#define USE_NLOHMANN_JSON 1
#define USE_FMT_TO_STRING 1

#include <stdint.h>
#include <assert.h>
#include "Point3.hpp"
#include "MathUtils.hpp"

#if USE_NLOHMANN_JSON
#include "nlohmann/json.hpp"
#endif

#if USE_FMT_TO_STRING
#include "spdlog/fmt/fmt.h"
#endif

namespace PocketSurvival::Engine
{
    struct Vector3
    {
        float posX;
        float posY;
        float posZ;

        constexpr Vector3() noexcept;
        constexpr Vector3(const Point3 &point) noexcept;
        constexpr Vector3(float x) noexcept;
        constexpr Vector3(float x, float y, float z) noexcept;

        constexpr inline Vector3 &operator+=(const Vector3 &right);
        constexpr inline Vector3 &operator-=(const Vector3 &right);
        constexpr inline Vector3 &operator*=(const Vector3 &right);
        constexpr inline Vector3 &operator/=(const Vector3 &right);

        constexpr inline Vector3 &operator+=(float c);
        constexpr inline Vector3 &operator-=(float c);
        constexpr inline Vector3 &operator*=(float c);
        constexpr inline Vector3 &operator/=(float c);
        constexpr inline Vector3 &operator=(const Vector3 &left);

        constexpr inline Point3 toPoint3() const;
        constexpr inline float volume() const;
        constexpr inline float lengthSquared() const;
        inline float length() const;

        friend constexpr inline bool operator==(const Vector3 &left, const Vector3 &right)
        {
            return (left.posX == right.posX) && (left.posY == right.posY) && (left.posZ == right.posZ);
        }
        friend constexpr inline bool operator!=(const Vector3 &left, const Vector3 &right)
        {
            return !(left == right);
        }
        friend constexpr inline Vector3 operator+(const Vector3 &left, const Vector3 &right)
        {
            return Vector3(left.posX + right.posX, left.posY + right.posY, left.posZ + right.posZ);
        }
        friend constexpr inline Vector3 operator-(const Vector3 &left, const Vector3 &right)
        {
            return Vector3(left.posX - right.posX, left.posY - right.posY, left.posZ - right.posZ);
        }
        friend constexpr inline Vector3 operator*(const Vector3 &left, float c)
        {
            return Vector3(left.posX * c, left.posY * c, left.posZ * c);
        }
        friend constexpr inline Vector3 operator*(float c, const Vector3 &right)
        {
            return Vector3(right.posX * c, right.posY * c, right.posZ * c);
        }
        friend constexpr inline Vector3 operator*(const Vector3 &left, const Vector3 &right)
        {
            return Vector3(left.posX * right.posX, left.posY * right.posY, left.posZ * right.posZ);
        }
        friend constexpr inline Vector3 operator/(const Vector3 &left, const Vector3 &right)
        {
            return Vector3(left.posX / right.posX, left.posY / right.posY, left.posZ / right.posZ);
        }

        friend constexpr inline Vector3 operator+(const Vector3 &left)
        {
            return left;
        }
        friend constexpr inline Vector3 operator-(const Vector3 &left)
        {
            return Vector3(-left.posX, -left.posY, left.posZ);
        }
        friend constexpr inline Vector3 operator/(const Vector3 &left, float divisor)
        {
            assert(divisor < 0 || divisor > 0);
            return Vector3(left.posX / divisor, left.posY / divisor, left.posZ / divisor);
        }

        static inline Vector3 Min(const Vector3& vec1, const Vector3 &vec2)
        {
            return Vector3(MathUtils::Min<float>(vec1.posX, vec2.posX), MathUtils::Min<float>(vec1.posY, vec2.posY), MathUtils::Min<float>(vec1.posZ, vec2.posZ));
        }
        static inline Vector3 Max(const Vector3& vec1, const Vector3 &vec2)
        {
            return Vector3(MathUtils::Max<float>(vec1.posX, vec2.posX), MathUtils::Max<float>(vec1.posY, vec2.posY), MathUtils::Max<float>(vec1.posZ, vec2.posZ));
        }
        static inline Vector3 Cross(const Vector3 &vec1, const Vector3 &vec2)
        {
            return Vector3( vec1.posY * vec2.posZ - vec2.posY * vec1.posZ, 
                            vec1.posZ * vec2.posX - vec2.posZ * vec1.posX,
                            vec1.posX * vec2.posY - vec2.posX * vec1.posY);
        }
        static inline float Dot(const Vector3 &vec1, const Vector3 &vec2)
        {
            return (vec1.posX * vec2.posX) + (vec1.posY * vec2.posY) + (vec1.posZ * vec2.posZ);
        }
        static inline Vector3 Normalize(const Vector3 &vec)
        {
            float num = vec.length();
            if((num > 0.0f) == false)
            {
                return Vector3{1.0f, 0.0f, 0.0f};
            }
            return vec / num;
        }
        static inline Vector3 Round(const Vector3 &v)
		{
			return Vector3(MathUtils::Round(v.posX), MathUtils::Round(v.posY), MathUtils::Round(v.posZ));
		}

		static inline float DistanceSquared(const Vector3 &v1, const Vector3 &v2)
		{
			return MathUtils::Sqr<float>(v1.posX - v2.posX) + MathUtils::Sqr<float>(v1.posY - v2.posY) + MathUtils::Sqr<float>(v1.posZ - v2.posZ);
		}
		static inline float Distance(const Vector3 &v1, const Vector3 &v2)
		{
			return MathUtils::Sqrt<float>(DistanceSquared(v1, v2));
		}
        static inline void Lerp(const Vector3 &v1, const Vector3 &v2, float f, Vector3 &result)
        {
            result.posX = MathUtils::Lerp(v1.posX, v2.posX, f);
            result.posY = MathUtils::Lerp(v1.posY, v2.posY, f);
            result.posZ = MathUtils::Lerp(v1.posZ, v2.posZ, f);
        }
        inline bool operator<(const Vector3& other) const
        {
            if(posX < other.posX)
            {
                return true;
            }
            else if(posX == other.posX)
            {
                if(posY < other.posY)
                {
                    return true;
                }
                else if(posY == other.posY)
                {
                    if(posZ < other.posZ)
                    {
                        return true;
                    }
                }
            }
            return false;
        }
        

#if USE_NLOHMANN_JSON
        friend inline void to_json(nlohmann::json &json, const Vector3 &vec);
        friend inline void from_json(const nlohmann::json &json, Vector3 &vec);
#endif

#if USE_FMT_TO_STRING
        inline std::string toString() const
        {
            return fmt::format("{},{},{}",posX,posY,posZ);
        }
#endif
    };

    constexpr inline Vector3::Vector3() noexcept : posX(0), posY(0), posZ(0) {}
    constexpr inline Vector3::Vector3(float x) noexcept : posX(x), posY(x), posZ(x) {}
    constexpr inline Vector3::Vector3(float x, float y, float z) noexcept : posX(x), posY(y), posZ(z) {}
    constexpr inline Vector3::Vector3(const Point3 &point) noexcept : posX(point.posX), posY(point.posY), posZ(point.posZ) {}

    constexpr inline Vector3 &Vector3::operator+=(const Vector3 &right)
    {
        posX += right.posX;
        posY += right.posY;
        posZ += right.posZ;
        return *this;
    }
    constexpr inline Vector3 &Vector3::operator-=(const Vector3 &right)
    {
        posX -= right.posX;
        posY -= right.posY;
        posZ -= right.posZ;
        return *this;
    }
    constexpr inline Vector3 &Vector3::operator*=(const Vector3 &right)
    {
        posX *= right.posX;
        posY *= right.posY;
        posZ *= right.posZ;
        return *this;
    }
    constexpr inline Vector3 &Vector3::operator/=(const Vector3 &right)
    {
        posX /= right.posX;
        posY /= right.posY;
        posZ /= right.posZ;
        return *this;
    }

    constexpr inline Vector3 &Vector3::operator+=(float c)
    {
        posX += c;
        posY += c;
        posZ += c;
        return *this;
    }
    constexpr inline Vector3 &Vector3::operator-=(float c)
    {
        posX -= c;
        posY -= c;
        posZ -= c;
        return *this;
    }
    constexpr inline Vector3 &Vector3::operator*=(float c)
    {
        posX *= c;
        posY *= c;
        posZ *= c;
        return *this;
    }
    constexpr inline Vector3 &Vector3::operator/=(float divisor)
    {
        assert(divisor > 0 || divisor < 0);
        posX /= divisor;
        posY /= divisor;
        posZ /= divisor;
        return *this;
    }

    constexpr inline Vector3 &Vector3::operator=(const Vector3 &left)
    {
        posX = left.posX;
        posY = left.posY;
        posZ = left.posZ;
        return *this;
    }

    constexpr inline Point3 Vector3::toPoint3() const
    {
        return Point3((int32_t)posX, (int32_t)posY, (int32_t)posZ);
    }
    constexpr inline float Vector3::volume() const
    {
        return posX * posY * posZ;
    }
    constexpr inline float Vector3::lengthSquared() const
    {
        return posX * posX + posY * posY + posZ * posZ;
    }
    inline float Vector3::length() const
    {
        return MathUtils::Sqrt<float>(lengthSquared());
    }

#if USE_NLOHMANN_JSON

    inline void to_json(nlohmann::json &json, const Vector3 &vec)
    {
        float xValue = std::isnan(vec.posX) ? 0 : vec.posX;
        float yValue = std::isnan(vec.posY) ? 0 : vec.posY;
        float zValue = std::isnan(vec.posZ) ? 0 : vec.posZ;

        json = {{"X",xValue},{"Y",yValue},{"Z",zValue}};
    }
    inline void from_json(const nlohmann::json &json, Vector3 &vec)
    {
        vec.posX = json["X"].get<float>();
        vec.posY = json["Y"].get<float>();
        vec.posZ = json["Z"].get<float>();
    }

#endif

    namespace FastVector3
    {
        constexpr inline Vector3 Zero{0.0f};
        constexpr inline Vector3 One{1.0f};
        constexpr inline Vector3 UnitX{1.0f, 0.0f, 0.0f};
        constexpr inline Vector3 UnitY{0.0f, 1.0f, 0.0f};
        constexpr inline Vector3 UnitZ{0.0f, 0.0f, 1.0f};

        constexpr inline Vector3 DecUnitX{-1.0f,  0.0f,  0.0f};
        constexpr inline Vector3 DecUnitY{ 0.0f, -1.0f,  0.0f};
        constexpr inline Vector3 DecUnitZ{ 0.0f,  0.0f, -1.0f};
    }

}

#undef USE_NLOHMANN_JSON
#undef USE_FMT_TO_STRING

#endif
